/**** Global variables. ****/

/* adapterEnv defined in adapter.js. */
var chatEnv = {
    /* CONFIG and ROUTES defined in chat.html. */
    CONFIG: CONFIG,
    ROUTES: ROUTES,
    MESSAGES: {
        "userMediaCaptured"   : "Waiting for access to media...",
        "waitingForConnection": "Waiting for someone to enter the room...",
        "connecting"          : "Connecting to user...",
        
        "nonWebRTCCompatibleBrowser": "Your browser does not appear to be WebRTC compatible. You won't be able to use this chat.",
        "getUserMediaFailed"        : "You must allow this page to capture your media to be able to chat!"
    },
    
    // If true, client is the one offering a transmission (caller). If false,
    // it's the one answering (reciever).
    caller: (CONFIG["caller"] == 0) ? false : true,

    /* DOM <video> elements */
    localVideo: null,
    remoteVideo: null,
    videoToggleButton: null,
    audioToggleButton: null,
    
    /* Media streams */
    localStream: null,
    remoteStream: null,
    
    /* Channel */
    channelOpened: false,
    channelSocket: null,
    channelRefreshTimer: null,

    
    /* Peer connection */
    peerConnectionStarted: false,
    peerConnection: null,
    // Used only by reciever.
    offerMessage: null,   // "offer" message that was recieved by client.
    iceCandidates: [],    // Ice candidates recieved, but not added.
    
    /* Settings */
    // Set up audio and video regardless of what devices are present.
    mediaConstraints: {
    	'mandatory' : {
    		'OfferToReceiveAudio' : true,
    		'OfferToReceiveVideo' : true
    	}
    },
    isVideoMuted: false,
    isAudioMuted: false,
    
    /* Status */
   isStatusShown: false
}



/**** Main function ****/

$(document).ready(function() {
    initializeConnection();
    initializeMedia();
    initializeTextChat();
    initializeFileUpload();
});



function initializeConnection() {
	chatEnv.localVideo = document.getElementById("local-video");
	chatEnv.remoteVideo = document.getElementById("remote-video");
	
	openChannel(chatEnv.CONFIG["token"]);
	
	/* Connection will be started when channel will be opened and media
	 * captured (see onChannelOpened and onUserMediaSuccess). For reciever also
	 * "offer" message must be recieved (see processSignalingMessage). */
}

/* When disconnecting channel must be properly closed. */
function addChannelCloseOnDisconnection() {
    // Refreshing or closing the page.
    window.onbeforeunload = function() {
        closeChannel();
    }
}



/**** Channel ****/

function openChannel(channelToken) {
	console.log("Opening channel.");
	var channel = new goog.appengine.Channel(channelToken);
	var handler = {
		'onopen': onChannelOpened,
		'onmessage': onChannelMessage,
		'onerror': onChannelError,
		'onclose': onChannelClosed
	};
	chatEnv.channelSocket = channel.open(handler);
	startChannelTokenRefresh();
}

function closeChannel() {
    sendByeMessage();
    chatEnv.channelSocket.close();
    stopChannelTokenRefresh();
}

// Closes the old channel and opens new one.
function reopenChannel(channelToken) {
    chatEnv.channelSocket.close();
    openChannel(channelToken);
}

function onChannelOpened() {
    console.log('Channel opened.');
    chatEnv.channelOpened = true;
    addChannelCloseOnDisconnection();
    maybeStartPeerConnection();
}

function onChannelMessage(message) {
    console.log('S->C: ' + message.data);
    processSignalingMessage(message.data);
}

function onChannelError(error) {
    console.log('Channel error: ' + error.description);
}

function onChannelClosed() {
    console.log('Channel closed.');
}

// Refresh the channel token periodically before it expires.
function startChannelTokenRefresh() {
    var interval = chatEnv.CONFIG["token_timeout"];   // In seconds.
    
    // Make sure the refresh timer is 10 sec less than token timeout.
    interval = (interval - 10 ) > 0 ? interval - 10 : interval;
    chatEnv.channelRefreshTimer = setInterval(function() {
        sendMessage({ type: 'tokenRequest' }, true)
    }, interval * 1000);
}

function stopChannelTokenRefresh() {
    if (chatEnv.channelRefreshTimer)
        clearInterval(chatEnv.channelRefreshTimer);
}



/**** User media ****/

function initializeMedia() {
    chatEnv.videoToggleButton = $("#video-toggle");
    chatEnv.audioToggleButton = $("#audio-toggle");
    
    captureUserMedia();
    addMediaMuteEvents();
}

function captureUserMedia() {
	// Call into getUserMedia via the polyfill (adapter.js).
	if (adapterEnv.getUserMedia) {
    	try {
    		adapterEnv.getUserMedia({
    			'audio' : true,
    			'video' : true
    		}, onUserMediaSuccess, onUserMediaError);
    	} catch (e) {
    		alert(chatEnv.MESSAGES.getUserMediaFailed);
    	}
    	showStatusMessage(chatEnv.MESSAGES.userMediaCaptured);
    }
    else {
        alert(chatEnv.MESSAGES.nonWebRTCCompatibleBrowser);
    }
}

function onUserMediaSuccess(stream) {
    console.log("User has granted access to local media.");
    // Attatch gotten media stream to local video.
    adapterEnv.attachMediaStream(chatEnv.localVideo, stream);
    chatEnv.localVideo.style.opacity = 1;
    chatEnv.localStream = stream;
    if (!chatEnv.caller)
        showStatusMessage(chatEnv.MESSAGES.waitingForConnection);
    maybeStartPeerConnection();
}

function onUserMediaError(error) {
    var message = "Failed to get access to local media. Error code was " + error.code;
    console.log(message);
    alert(message + ".");
}

function addMediaMuteEvents() {
    chatEnv.videoToggleButton.click(toggleVideoMute);
    chatEnv.audioToggleButton.click(toggleAudioMute);
}

/* Ctrl-D: toggle audio mute; Ctrl-E: toggle video mute.
 * On Mac, Command key is instead of Ctrl.
 * Return false to screen out original Chrome shortcuts. */
function addMediaMuteKeyboardEvents() {
    document.onkeydown = function() {
        if (navigator.appVersion.indexOf("Mac") != -1) {
            if (event.metaKey && event.keyCode === 68) {
                toggleAudioMute();
                return false;
            }
            if (event.metaKey && event.keyCode === 69) {
                toggleVideoMute();
                return false;
            }
        } else {
            if (event.ctrlKey && event.keyCode === 68) {
                toggleAudioMute();
                return false;
            }
            if (event.ctrlKey && event.keyCode === 69) {
                toggleVideoMute();
                return false;
            }
        }
    }
}

function toggleVideoMute() {
    if (!chatEnv.localStream) {
        return;
    }
    
    var localVideoTracks = chatEnv.localStream.getVideoTracks();
    
    if (localVideoTracks.length === 0) {
        console.log("No local video available.");
        return;
    }

    if (chatEnv.isVideoMuted) {
        for ( i = 0; i < localVideoTracks.length; i++) {
            localVideoTracks[i].enabled = true;
        }
        console.log("Video unmuted.");
        chatEnv.videoToggleButton.html("Video On");
    } else {
        for ( i = 0; i < localVideoTracks.length; i++) {
            localVideoTracks[i].enabled = false;
        }
        console.log("Video muted.");
        chatEnv.videoToggleButton.html("Video Off");
    }

    chatEnv.isVideoMuted = !chatEnv.isVideoMuted;
}

function toggleAudioMute() {
    if (!chatEnv.localStream) {
        return;
    }
    
    var localAudioTracks = chatEnv.localStream.getAudioTracks();
    
    if (localAudioTracks.length === 0) {
        console.log("No local audio available.");
        return;
    }

    if (chatEnv.isAudioMuted) {
        for ( i = 0; i < localAudioTracks.length; i++) {
            localAudioTracks[i].enabled = true;
        }
        console.log("Audio unmuted.");
        chatEnv.audioToggleButton.html("Audio On");
    } else {
        for ( i = 0; i < localAudioTracks.length; i++) {
            localAudioTracks[i].enabled = false;
        }
        console.log("Audio muted.");
        chatEnv.audioToggleButton.html("Audio Off");
    }

    chatEnv.isAudioMuted = !chatEnv.isAudioMuted;
}



/**** Peer connection. ****/

function maybeStartPeerConnection() {
    if (!chatEnv.peerConnectionStarted && chatEnv.localStream && chatEnv.channelOpened) {
        // If client is a reciever, make sure that the offer's been already recieved.
        if (!chatEnv.caller && !chatEnv.offerMessage) {
            return;
        }
        
        showStatusMessage(chatEnv.MESSAGES.connecting);
        createRTCPeerConnectionObj();
        
        console.log("Adding local stream.");
        chatEnv.peerConnection.addStream(chatEnv.localStream);
        chatEnv.peerConnectionStarted = true;
        
        if (chatEnv.caller) {
            sendOffer();
        } else {            
            // Setting the description we got with "offer" message. Couldn't
            // do that when the message was recieved, because RTCPeerConnection
            // hasn't yet been created then.
            chatEnv.peerConnection.setRemoteDescription(
                new RTCSessionDescription(chatEnv.offerMessage)
            );
            sendAnswer();
        }
        
        // Client has already recieved some candidates to add.
        if (chatEnv.iceCandidates.length > 0) {
            var candidate;
            
            console.log("Adding saved ice candidates.");
            while (chatEnv.iceCandidates.length > 0) {
                candidate = chatEnv.iceCandidates.pop();
                chatEnv.peerConnection.addIceCandidate(candidate);
            }
        }
    }
}

function stopPeerConnection() {
    chatEnv.peerConnectionStarted = false;
    chatEnv.isAudioMuted = false;
    chatEnv.isVideoMuted = false;
    chatEnv.peerConnection.close();
    chatEnv.peerConnection = null;
}

function createRTCPeerConnectionObj() {    
    console.log("Creating PeerConnection.");
	try {
		// Create an RTCPeerConnection via the polyfill (adapter.js).
		chatEnv.peerConnection = new adapterEnv.RTCPeerConnection(
		    chatEnv.CONFIG["peer_connection_config"]
		);
		console.log("Created RTCPeerConnnection with config:\n" + "  \"" + JSON.stringify(chatEnv.CONFIG["peer_connection_config"]) + "\".");
	} catch (e) {
		console.log("Failed to create PeerConnection, exception: " + e.message);
		alert("Cannot create RTCPeerConnection object; WebRTC is not supported by this browser.");
		return;
	}

    chatEnv.peerConnection.onicecandidate = onIceCandidate;
	chatEnv.peerConnection.onconnecting = onSessionConnecting;
	chatEnv.peerConnection.onopen = onSessionOpened;
	chatEnv.peerConnection.onaddstream = onRemoteStreamAdded;
	chatEnv.peerConnection.onremovestream = onRemoteStreamRemoved;
}

function onIceCandidate(event) {
    if (event.candidate) {
        sendMessage({
            type : 'candidate',
            label : event.candidate.sdpMLineIndex,
            id : event.candidate.sdpMid,
            candidate : event.candidate.candidate
        }, true);
    } else {
        console.log("End of candidates.");
    }
}

function onSessionConnecting(message) {
    console.log("Session connecting.");
}

function onSessionOpened(message) {
    console.log("Session opened.");
}

function onRemoteStreamAdded(event) {
    console.log("Remote stream added.");
    adapterEnv.attachMediaStream(chatEnv.remoteVideo, event.stream);
    chatEnv.remoteStream = event.stream;
    waitForRemoteVideo();
}

function onRemoteStreamRemoved(event) {
    console.log("Remote stream removed.");
}

function sendOffer() {
    console.log("Sending offer to peer.");
    chatEnv.peerConnection.createOffer(onDescription, null, chatEnv.mediaConstraints);
}


function sendAnswer() {
    console.log("Sending answer to peer.");
    chatEnv.peerConnection.createAnswer(onDescription, null, chatEnv.mediaConstraints);
}

function onDescription(sessionDescription) {
    // Set Opus as the preferred codec in SDP if Opus is present.
    sessionDescription.sdp = preferOpus(sessionDescription.sdp);
    chatEnv.peerConnection.setLocalDescription(sessionDescription);
    sendMessage(sessionDescription, true);
}

function waitForRemoteVideo() {
    if (chatEnv.remoteStream.getVideoTracks().length === 0 || chatEnv.remoteVideo.currentTime > 0) {
        /* Connection fully established. */
       chatEnv.remoteVideo.style.opacity = 1;
        hideStatus();
    } else {
        setTimeout(waitForRemoteVideo, 100);
    }
}



/**** Messages ****/

/* Sends messageObj to server via XHR POST request and JSON encoding.
   asynchronous defines if using asynchronous (or synchronous) XHR request. */
function sendMessage(messageObj, asynchronous) {
	var msgJSON = JSON.stringify(messageObj);
	console.log('C->S: ' + msgJSON);
	path = ROUTES["send_message_url"];
	var xhr = new XMLHttpRequest();
	xhr.open('POST', path, asynchronous);
	xhr.send(msgJSON);
}

function sendByeMessage() {
    sendMessage({
        type: 'bye'
    }, false);
}

/* Process message recieved from server. */
function processSignalingMessage(messageJSON) {
	var messageObj = JSON.parse(messageJSON);

    /* SDP offer */
	if (messageObj.type === 'offer') {
	    /* We can't set remote description right away, because
	     * peer connection hasn't been created yet. */
	    chatEnv.offerMessage = messageObj;
	    showChatMessage(chatEnv.offerMessage);
	    maybeStartPeerConnection();
	/* SDP answer */
	} else if (messageObj.type === 'answer' && chatEnv.peerConnectionStarted) {
		chatEnv.peerConnection.setRemoteDescription(new RTCSessionDescription(messageObj));
    /* Ice candidate */
	} else if (messageObj.type === 'candidate') {
		var candidate = new RTCIceCandidate({
			sdpMLineIndex : messageObj.label,
			candidate : messageObj.candidate
		});
		
		if (chatEnv.peerConnectionStarted) {
		    chatEnv.peerConnection.addIceCandidate(candidate);
		}
		// If peerConnection still not created, save the candidate so it can
		// be added later.
		else {
		    console.log("Saving ice candidate.")
		    chatEnv.iceCandidates.push(candidate);
		}
    /* Bye - peer disconnecting from room. */
	} else if (messageObj.type === 'bye' && chatEnv.peerConnectionStarted) {
	    showChatMessage(messageObj);
		onRemoteHangUp();
    /* Token response */
	} else if (messageObj.type === 'tokenResponse') {
		reopenChannel(messageObj.token);
	}
	/* Chat message */
	else if (messageObj.type === 'chatMessage') {
        showChatMessage(messageObj);
    }
    else if (messageObj.type == "fileUpload") {
        showChatMessage(messageObj);
    }
    else {
        console.log("Invalid message of type: " + messageObj.type)
    }
}



/**** Hanging up ****/

function hangUp() {
    console.log("Hanging up.");
    chatEnv.localVideo.style.opacity = 0;
    chatEnv.remoteVideo.style.opacity = 0;
    stopPeerConnection();
}

function onRemoteHangUp() {
    console.log('Session terminated.');
    setTimeout(function() {
        chatEnv.remoteVideo.src = ""
    }, 500);
    chatEnv.remoteVideo.style.opacity = 0;
    stopPeerConnection();
    chatEnv.caller = false;
    showStatusMessage(chatEnv.MESSAGES.waitingForConnection);
}



/**** Text chat ****/

function initializeTextChat() {
    // Submitting new message.
    $("#new-message").submit(function() {
        var content = $("#new-message-content").val();
        if (content) {
            sendChatMessage(content);
        }
        $("#new-message-content").val("");   // Empty the input.
        return false;   // Prevent form from submitting.
    });
}

function sendChatMessage(content) {
    var message = {
        "type" : "chatMessage",
        "content": content,
        "sender": chatEnv.CONFIG["my_username"]
    };
	sendMessage(message, true);
	showChatMessage(message);
}

function showChatMessage(messageObj) {
    var toShow;
    var type = messageObj.type;
    var messageList = $("#messages ul");
    
    if (type == "chatMessage") {
        toShow = "<span class='message-username'>" + messageObj.sender + "</span>: " +
                 "<span class='message-content'>" + messageObj.content + "</span>";
    } else if (type == "fileUpload") {
        toShow = "<span class='message-username'>" + messageObj.uploader + "</span> " +
                 "<span class='message-event'>uploaded file <a target='_blank' href='" +
                  chatEnv.ROUTES["file_download_url"] + messageObj.blobInfo.key +
                  "'>" + messageObj.blobInfo.filename + "</a></span>.";
    } else if (type == "bye") {
        toShow = "<span class='message-username'>" + messageObj.sender + "</span> " +
                 "quit from the room.";
    }
    else if (type == "offer") {
        toShow = "<span class='message-username'>" + messageObj.sender + "</span> " +
                 "entered the room.";
    }
    
    
    // Add message the list.
    messageList.append($("<li></li>").html(toShow));
    // Adding scrollbar if needed.
    $(".scrollable").nanoScroller({ contentClass: "scrollable-content", alwaysVisible: true });
    // Scroll down the list.
    messageList.scrollTop(messageList.prop("scrollHeight"));
}



/**** File upload ****/

function initializeFileUpload() {
    loadFileUploadForm();
    $("#file-upload-button").click(function() {
        // Box to select a file appears.
        $("#file-select").trigger("click");
    });
}

// After every POST upload you have to reload form, so that it works again.
function loadFileUploadForm() {
    $.get(ROUTES["file_upload_form_url"], function(data) {
        $("#file-upload-form-parent").html("").html(data);
        loadUploadEvents();
    }); 
}

function loadUploadEvents(){
	var ajaxOptions = {
		success: sendFileUploadedMessage,
		dataType: 'json'
	};

    // When a file is selected.
	$('#file-select').on('change', function() {
		$("#file-upload-form-parent form").ajaxSubmit(ajaxOptions);
		return false;
	});
}

/* Send a message to the server that the file was uploaded.
 * Server brodcasts the upload to all the users in the room. */
function sendFileUploadedMessage(response, statusText) {
    var message = {
        "type": "fileUpload",
        "blobInfo": response,
        "uploader": chatEnv.CONFIG["my_username"]
    }
    sendMessage(message, true);
    showChatMessage(message, true);

    // Re-load the form. You have to make new session for every upload.
    loadFileUploadForm();
}



/**** Status ****/

function showStatusMessage(message) {
    $("#status").html(message);
    if (!chatEnv.isStatusShown)
        $("#status").slideDown(100);
}

function hideStatus() {
    $("#status").slideUp(100).html("");
    chatEnv.isStatusShown = false;
}